/**
 * Title: Hyperbolic engine
 * Description: jFamilyTree Engine
 * Version: provider
 * Copyright: (c) 2001-2008
 * Terms of use:see license agreement at http://treebolic.sourceforge.net/en/license.htm
 * Author: Bernard Bou
 * Company: bsys
 * Update: Mon Mar 10 00:00:00 CEST 2008
 */
package jFamilyTree.view;

import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.ActionEvent;

import javax.swing.AbstractAction;
import javax.swing.JPanel;
import javax.swing.Timer;

import jFamilyTree.control.Controller;
import jFamilyTree.control.MouseAdapter;
import jFamilyTree.core.AbstractLayerOut;
import jFamilyTree.core.Complex;
import jFamilyTree.core.HyperRotation;
import jFamilyTree.core.HyperTransform;
import jFamilyTree.core.Transformer;
import jFamilyTree.model.INode;
import jFamilyTree.model.Location;
import jFamilyTree.model.Model;
import jFamilyTree.model.Settings;

/**
 * View
 * 
 * @author Bernard Bou
 */
public class View extends JPanel
{
	static private final long serialVersionUID = 1L;

	// D A T A

	/**
	 * Data model
	 */
	private Model theModel;

	// A G E N T S

	/**
	 * Painter
	 */
	private final AbstractPainter thePainter;

	/**
	 * Transformer
	 */
	private final Transformer theTransformer;

	/**
	 * Animator
	 */
	private final Animator theAnimator;

	/**
	 * LayerOut
	 */
	private AbstractLayerOut theLayerOut;

	/**
	 * Mouse adapter
	 */
	private MouseAdapter theMouseAdapter;

	/**
	 * Timer
	 */
	private Timer theTimer;

	// G R A P H I C S

	/**
	 * Full redraw flag
	 */
	private boolean invalidatePainterGraphics;

	/**
	 * View size
	 */
	private Dimension theSize;

	/**
	 * Drawing cache
	 */
	private Image theCache;

	// R E F E R E N C E . N O D E S

	/**
	 * Hot node
	 */
	private INode theHotNode;

	/**
	 * Focus node
	 */
	private INode theFocusNode;

	// B E H A V I O U R

	/**
	 * Whether hovering on nodes triggers gaining focus
	 */
	private boolean focusOnHover;

	/**
	 * Whether it has tooltip
	 */
	private boolean hasTooltip;

	/**
	 * Whether tooltips display contents
	 */
	public boolean tooltipDisplaysContent;

	/**
	 * Tooltip break
	 */
	static private int theTooltipLineSpan = 80;

	// A N I M A T I O N

	/**
	 * Animation time slice
	 */
	static private final int ANIMATIONTIMESLICE = 100;

	/**
	 * Animation start delay
	 */
	static private final int ANIMATIONSTARTDELAY = 1000;

	/**
	 * Hot node time slice
	 */
	static private final int HOTNODETIMESLICE = 1000;

	// C O N S T R U C T O R

	/**
	 * Constructor
	 */
	public View()
	{
		// graphics
		this.setLayout(null);

		// painter
		thePainter = new Painter();
		thePainter.resetColors();

		// transformer
		theTransformer = new Transformer();

		// animator
		theAnimator = new Animator(this, ANIMATIONTIMESLICE, ANIMATIONSTARTDELAY);

		// graphics
		theSize = new Dimension(0, 0);
		theCache = null;
		invalidatePainterGraphics = true;

		// reference nodes
		theHotNode = null;
		theFocusNode = null;

		// behaviour flags
		focusOnHover = true;
		hasTooltip = true;
		tooltipDisplaysContent = true;
	}

	// C O N N E C T

	/**
	 * Connect controller
	 * 
	 * @param thisController
	 *        controller
	 */
	public void connect(Controller thisController)
	{
		// mouse
		theMouseAdapter = new MouseAdapter(thisController);
		this.addMouseListener(theMouseAdapter);
		this.addMouseMotionListener(theMouseAdapter);
	}

	/**
	 * Set model
	 * 
	 * @param thisModel
	 *        data model
	 */
	public void connect(Model thisModel)
	{
		theModel = thisModel;
		theAnimator.start();
	}

	/**
	 * Connect layout agent
	 * 
	 * @param thisLayerOut
	 *        layout agent
	 */
	public void connect(AbstractLayerOut thisLayerOut)
	{
		theLayerOut = thisLayerOut;
	}

	// P A I N T I N G

	/*
	 * (non-Javadoc)
	 * 
	 * @see jFamilyTree.appletcore.IView#repaint()
	 */
	public void repaint()
	{
		if (theMouseAdapter != null)
			theMouseAdapter.resetHotNode();
		super.repaint();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.swing.JComponent#paint(java.awt.Graphics)
	 */
	public void paint(Graphics thisScreenGraphics)
	{
		// drag
		if (theMouseAdapter.drag())
			invalidatePainterGraphics = true;

		// create a cached image the first time or when size changes
		Dimension thisSize = getSize();
		if (theCache == null || thisSize.width != theSize.width || thisSize.height != theSize.height)
		{
			// invalidate old cache
			invalidatePainterGraphics = true;

			// size
			theSize = thisSize;

			// cache
			theCache = createImage(theSize.width, theSize.height);
		}

		// setup painter cached graphics and size
		if (invalidatePainterGraphics)
		{
			// cached graphics context
			Graphics thisCacheGraphics = theCache.getGraphics();

			// init
			thisCacheGraphics.setFont(getFont());

			// tell painter
			thePainter.setup(thisCacheGraphics, theSize);
		}

		// paint background
		thePainter.paintBackground();

		// paint tree
		thePainter.paint(theModel.theTree.getRoot(), theModel.theTree.getEdges());

		// transfer cache on screen
		thisScreenGraphics.drawImage(theCache, 0, 0, null);
	}

	/**
	 * Invalidate graphics and repaint
	 */
	private void invalidateAndRepaint()
	{
		invalidatePainterGraphics = true;
		repaint();
	}

	/**
	 * Set default cursor
	 * 
	 * @param linkCursor
	 *        whether to use 'link' cursor
	 */
	public void setHoverCursor(boolean linkCursor)
	{
		if (linkCursor)
			setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
		else
			setCursor(Cursor.getDefaultCursor());
	}

	// D R A G . N O T I F I C A T I O N

	/**
	 * Drag notification hook
	 */
	public void enterDrag()
	{
		thePainter.enterDrag();
		invalidateAndRepaint();
	}

	/**
	 * Leave drag notification hook
	 */
	public void leaveDrag()
	{
		thePainter.leaveDrag();
		invalidateAndRepaint();
	}

	// M O U N T . N O T I F I C A T I O N

	/**
	 * Mount notification hook
	 * 
	 * @param thisMountedRoot
	 *        mounted node
	 */
	public void mount(INode thisMountedRoot)
	{
		theTransformer.transform(thisMountedRoot);
		repaint();
	}

	/**
	 * Unmount notification hook
	 */
	public void umount(INode thisMountingRoot)
	{
		theTransformer.transform(thisMountingRoot);
		repaint();
	}

	// F O C U S

	/**
	 * Control whether hovering on node triggers gaining focus
	 * 
	 * @param thisFlag
	 *        whether hovering on node triggers gaining focus (null toggles value)
	 */
	@SuppressWarnings("serial")
	public void setFocusOnHover(Boolean thisFlag)
	{
		focusOnHover = thisFlag != null ? thisFlag : !focusOnHover;

		// implement
		if (focusOnHover)
		{
			// timer creation
			if (theTimer == null)
			{
				theTimer = new Timer(HOTNODETIMESLICE, new AbstractAction()
				{
					public void actionPerformed(ActionEvent e)
					{
						INode thisHotNode = theMouseAdapter.getHotNode();
						boolean again = (theHotNode == thisHotNode);
						theHotNode = thisHotNode;
						if (!again || theAnimator.isRunning() || theHotNode == null || theHotNode.getLocation().hyper.center.equals(Complex.ZERO))
							return;
						animateToCenter(theHotNode, true);
					}
				});
				theTimer.setRepeats(true);
			}

			// start
			theTimer.start();
		}
		else
		{
			if (theTimer != null)
				theTimer.stop();
		}
	}

	// O R I E N T A T I O N

	/**
	 * Preserve orientation
	 * 
	 * @param thisFlag
	 *        whether transformations preserve orientation (null toggles value)
	 */
	public void setPreserveOrientation(Boolean thisFlag)
	{
		theTransformer.setPreserveOrientation(thisFlag != null ? thisFlag : !theTransformer.getPreserveOrientation());
	}

	// O F F S E T

	/**
	 * Set shift
	 * 
	 * @param cx
	 *        x shift
	 * @param cy
	 *        y shift
	 * @param xinc
	 *        whether to add to current x shift
	 * @param yinc
	 *        whether to add to current y shift
	 */
	public void setShift(double cx, double cy, boolean xinc, boolean yinc)
	{
		if (xinc)
			cx += thePainter.getXShift();
		if (yinc)
			cy += thePainter.getYShift();
		thePainter.setXShift(cx);
		thePainter.setYShift(cy);
	}

	/**
	 * Set zoom factor
	 * 
	 * @param f
	 *        zoom factor
	 */
	public void setZoomFactor(double f)
	{
		thePainter.setZoomFactor(f);
	}

	// R E N D E R I N G

	/**
	 * Set linear/hyperbolic rendering of edges
	 * 
	 * @param thisFlag
	 *        whether edges are rendered in hyperbolic way (null toggles value)
	 */
	public void setHyperbolicEdges(Boolean thisFlag)
	{
		thePainter.setHyperbolicEdges(thisFlag != null ? thisFlag : !thePainter.getHyperbolicEdges());
	}

	// T O O L T I P

	/**
	 * Enable/disable tooltips
	 * 
	 * @param thisFlag
	 *        whether to display tooltip (null toggles value)
	 */
	public void setHasTooltip(Boolean thisFlag)
	{
		hasTooltip = thisFlag != null ? thisFlag : !hasTooltip;
		setToolTipText(null);
	}

	/**
	 * Enable/disable displaying content in tooltips
	 * 
	 * @param thisFlag
	 *        whether tooltip displays content (null toggles value)
	 */
	public void setTooltipDisplaysContent(Boolean thisFlag)
	{
		tooltipDisplaysContent = thisFlag != null ? thisFlag : !tooltipDisplaysContent;
	}

	/**
	 * Set tooltip to contain information about this node
	 * 
	 * @param thisNode
	 *        node
	 */
	public void setToolTip(INode thisNode)
	{
		if (!hasTooltip)
			return;

		String thisLabel = thisNode.getLabel();
		String thisContent = thisNode.getContent();
		if (thisLabel == null && (tooltipDisplaysContent ? thisContent == null : true))
			return;

		StringBuffer thisBuffer = new StringBuffer();
		thisBuffer.append("<html>");

		if (thisLabel != null && !thisLabel.isEmpty())
		{
			thisBuffer.append("<strong>");
			thisBuffer.append(thisLabel);
			thisBuffer.append("</strong><br>");
		}

		if (tooltipDisplaysContent)
		{
			if (thisContent != null && !thisContent.isEmpty())
			{
				String[] theseLines = thisContent.split("\n");
				for (String thisLine : theseLines)
				{
					StringBuffer thisLineBuffer = new StringBuffer(thisLine);

					// force break after x characters
					for (int offset = theTooltipLineSpan; offset < thisLineBuffer.length(); offset += theTooltipLineSpan)
						thisLineBuffer.insert(offset, "<br>");

					// append processed line with break
					thisBuffer.append(thisLineBuffer);
					thisBuffer.append("<br>");
				}
			}
		}
		thisBuffer.append("</html>");
		setToolTipText(thisBuffer.toString());
	}

	// P O P U P

	/**
	 * Control whether view has popup menu
	 * 
	 * @param thisFlag
	 *        whether view has popup menu (null toggles value)
	 */
	public void setPopUpMenu(Boolean thisFlag)
	{
		theMouseAdapter.hasPopUp = thisFlag != null ? thisFlag : !theMouseAdapter.hasPopUp;
	}

	// A P P L Y . S E T T I N G S

	/**
	 * Set view behaviour
	 * 
	 * @param theseSettings
	 *        settings
	 */
	public void apply(Settings theseSettings)
	{
		// view settings
		if (theseSettings.theHasPopUpMenuFlag != null)
			setPopUpMenu(theseSettings.theHasPopUpMenuFlag);
		if (theseSettings.theFocusOnHoverFlag != null)
			setFocusOnHover(theseSettings.theFocusOnHoverFlag);
		if (theseSettings.theHasToolTipFlag != null)
			setHasTooltip(theseSettings.theHasToolTipFlag);
		if (theseSettings.theToolTipDisplaysContentFlag != null)
			setTooltipDisplaysContent(theseSettings.theToolTipDisplaysContentFlag);
		if (theseSettings.thePreserveOrientationFlag != null)
			setPreserveOrientation(theseSettings.thePreserveOrientationFlag);
		if (theseSettings.theXShift != null || theseSettings.theYShift != null)
			setShift(theseSettings.theXShift == null ? 0. : theseSettings.theXShift, theseSettings.theYShift == null ? 0. : theseSettings.theYShift, false, false);

		// painter
		thePainter.setColors(theseSettings.theBackColor, theseSettings.theForeColor, theseSettings.theNodeBackColor, theseSettings.theNodeForeColor, theseSettings.theTreeEdgeColor, theseSettings.theEdgeColor);
		thePainter.setFonts(theseSettings.theFontFace, theseSettings.theFontSize, theseSettings.theFontSizeStep);
		thePainter.setHyperbolicEdges(theseSettings.theHyperbolicEdgesFlag);
		thePainter.setEdgeStyles(theseSettings.theTreeEdgeStyle, theseSettings.theEdgeStyle);
	}

	/**
	 * Set images
	 * 
	 * @param thisBackgroundImage
	 *        background image
	 * @param thisDefaultNodeImage
	 *        default node image
	 * @param thisDefaultTreeEdgeImage
	 *        default tree edge image
	 * @param thisDefaultEdgeImage
	 *        default edge image
	 */
	public void setImages(Image thisBackgroundImage, Image thisDefaultNodeImage, Image thisDefaultTreeEdgeImage, Image thisDefaultEdgeImage)
	{
		thePainter.setImages(thisBackgroundImage, thisDefaultNodeImage, thisDefaultTreeEdgeImage, thisDefaultEdgeImage);
	}

	// S E T . T R A N S F O R M

	public void resetTransform()
	{
		theTransformer.setTransform(HyperTransform.NULLTRANSFORM);
	}

	// A P P L Y . T R A N S F O R M

	/**
	 * Compose this transform with current and apply
	 * 
	 * @param thisTransform
	 *        transform to compose with current transform
	 */
	public void applyComposedTransform(HyperTransform thisTransform)
	{
		theTransformer.composeTransform(thisTransform);
		theTransformer.transform(theModel.theTree.getRoot());
	}

	/**
	 * Apply transform
	 * 
	 * @param thisTransform
	 *        transform
	 */
	public void applyTransform(HyperTransform thisTransform)
	{
		theTransformer.setTransform(thisTransform);
		theTransformer.transform(theModel.theTree.getRoot());
	}

	/**
	 * Apply null transform
	 */
	public void applyNullTransform()
	{
		theTransformer.reset(theModel.theTree.getRoot());
	}

	/**
	 * Apply initial transform
	 */
	public void applyInitialTransform()
	{
		Complex thisStart = new Complex(-1, -1).normalize().multiply(0.9);
		HyperTransform thisTransform = theTransformer.makeTransform(thisStart, Complex.ZERO, theLayerOut.getOrientation());
		applyTransform(thisTransform);
	}

	// D E L T A . M O V E

	/**
	 * Compose delta translation
	 * 
	 * @param thisStart
	 *        unit circle delta vector start
	 * @param thisEnd
	 *        unit circle delta vector start
	 */
	public void composeTranslate(Complex thisStart, Complex thisEnd)
	{
		HyperTransform thisTransform = theTransformer.makeTransform(thisStart, thisEnd, theLayerOut.getOrientation());
		applyComposedTransform(thisTransform);
		repaint();
	}

	/**
	 * Compose delta rotation
	 * 
	 * @param thisStart
	 *        unit circle delta vector start
	 * @param thisEnd
	 *        unit circle delta vector end
	 */
	public void composeRotate(Complex thisStart, Complex thisEnd)
	{
		HyperRotation thisRotation = new HyperRotation(Complex.ONE);
		thisRotation.div(thisEnd, thisStart);
		thisRotation.normalize();
		applyComposedTransform(new HyperTransform(thisRotation));
		repaint();
	}

	// M O V E

	/**
	 * Translate tree according to vector
	 * 
	 * @param thisStart
	 *        unit circle vector start
	 * @param thisEnd
	 *        unit circle vector end
	 */
	private void translate(Complex thisStart, Complex thisEnd)
	{
		HyperTransform thisTransform = theTransformer.makeTransform(thisStart, thisEnd, theLayerOut.getOrientation());
		applyTransform(thisTransform);
		repaint();
	}

	/**
	 * Translate node to point
	 * 
	 * @param thisNode
	 *        node
	 * @param thisDestination
	 *        unit circle destination location
	 */
	public void moveTo(INode thisNode, Complex thisDestination)
	{
		Location thisLocation = thisNode.getLocation();
		translate(thisLocation.hyper.center0, thisDestination);
		theFocusNode = thisNode;
	}

	/**
	 * Translate node to unit circle center
	 * 
	 * @param thisNode
	 *        node
	 */
	public void moveToCenter(INode thisNode)
	{
		Location thisLocation = thisNode.getLocation();
		translate(thisLocation.hyper.center0, Complex.ZERO);
		theFocusNode = thisNode;
	}

	// A N I M A T E

	/**
	 * Animate to unit circle center
	 * 
	 * @param thisNode
	 *        node
	 * @param now
	 *        whether to start now
	 */
	public void animateToCenter(INode thisNode, boolean now)
	{
		Location thisLocation = thisNode.getLocation();
		Complex thisFrom = thisLocation.hyper.center;
		animate(thisFrom, Complex.ZERO, now);
		theFocusNode = thisNode;
	}

	/**
	 * Animate node to destination location
	 * 
	 * @param thisNode
	 *        node
	 * @param thisDestination
	 *        unit circle destination location
	 * @param now
	 *        whether to start now
	 */
	public void animateTo(INode thisNode, Complex thisDestination, boolean now)
	{
		Location thisLocation = thisNode.getLocation();
		Complex thisFrom = thisLocation.hyper.center;
		animate(thisFrom, thisDestination, now);
		theFocusNode = thisNode;
	}

	/**
	 * Animate tree. Translate as per vector(start,end)
	 * 
	 * @param thisFrom
	 *        unit circle vector start
	 * @param thisTo
	 *        unit circle vector end
	 * @param now
	 *        whether to start now
	 */
	private synchronized void animate(Complex thisFrom, Complex thisTo, boolean now)
	{
		Animation thisAnimation = Animation.make(thisFrom, thisTo, theTransformer, theLayerOut.getOrientation(), 0);
		theAnimator.run(thisAnimation, now);
	}

	// S E A R C H I N G

	/**
	 * Find node given view location
	 * 
	 * @param vx
	 *        view x location
	 * @param vy
	 *        view y location
	 * @return node
	 */
	public INode findNode(int vx, int vy)
	{
		Complex thisEuclidianLocation = thePainter.viewToUnitCircle(vx, vy, theSize);
		return theModel.theTree.findNodeAt(theModel.theTree.getRoot(), thisEuclidianLocation);
	}

	/**
	 * Get focused node
	 * 
	 * @return focus noe (at center of unit circle)
	 */
	public INode getFocusNode()
	{
		return theFocusNode;
	}

	// S P A C E . C O N V E R S I O N

	/**
	 * Convert from view coordinates to unit circle euclidian coordinates
	 * 
	 * @param vx
	 *        view x-coordinate
	 * @param vy
	 *        view x-coordinate
	 * @return unit circle euclidian coordinate
	 */
	public Complex viewToUnitCircle(int vx, int vy)
	{
		return thePainter.viewToUnitCircle(vx, vy, getSize());
	}
}
